use std::ops::Add;

use Shape::{Circle as OtherCircle, Polygon as OtherPolygon};

#[derive(Clone, Copy, PartialEq, Debug)]
pub struct Point {
    // add fields
    x: i32,
    y: i32,
}

impl Point {
    // add methods
    pub fn new(x: i32, y: i32) -> Point {
        Point { x, y }
    }

    //this method means the distance from (0,0)
    pub fn magnitude(&self) -> f64 {
        self.dist(Point { x: 0, y: 0 })
    }

    pub fn dist(&self, other: Point) -> f64 {
        (((self.x - other.x).pow(2) + (self.y - other.y).pow(2)) as f64).sqrt()
    }
}

impl Add for Point {
    type Output = Self;

    fn add(self, other: Self) -> Self::Output {
        Self {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

pub struct Polygon {
    points: Vec<Point>,
}

impl Polygon {
    pub fn new() -> Self {
        Polygon { points: vec![] }
    }

    pub fn add_point(&mut self, point: Point) {
        self.points.push(point)
    }

    pub fn left_most_point(&self) -> Option<Point> {
        self.points.clone().into_iter().min_by_key(|&v| v.x)
    }

    pub fn iter(&self) -> std::slice::Iter<'_, Point> {
        self.points.iter()
    }

    pub fn sorted_points(&self) -> Vec<Point> {
        let mut vec = self.points.clone();
        vec.sort_by(|v1, v2| v1.x.cmp(&v2.x).then(v1.y.cmp(&v2.y)));
        vec
    }
}

pub struct Circle {
    center: Point,
    radius: i32,
}

impl Circle {
    pub fn new(center: Point, radius: i32) -> Self {
        Circle { center, radius }
    }
}

pub enum Shape {
    Polygon(Polygon),
    Circle(Circle),
}

impl From<Polygon> for Shape {
    fn from(value: Polygon) -> Self {
        OtherPolygon(value)
    }
}

impl From<Circle> for Shape {
    fn from(value: Circle) -> Self {
        OtherCircle(value)
    }
}

impl Shape {
    pub fn perimeter(&self) -> f64 {
        match self {
            OtherPolygon(p) => {
                let vec = p.sorted_points();
                let mut perimete: f64 = 0.0;
                for i in 0..(vec.len() - 1) {
                    let dis = vec.get(i).unwrap().dist(*vec.get(i + 1).unwrap());
                    perimete += dis;
                }
                let v = vec.first().unwrap().dist(*vec.last().unwrap());
                perimete += v;
                perimete
            }
            OtherCircle(c) => c.radius as f64 * 2f64 * std::f64::consts::PI
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn round_two_digits(x: f64) -> f64 {
        (x * 100.0).round() / 100.0
    }

    #[test]
    fn test_point_magnitude() {
        let p1 = Point::new(12, 13);
        assert_eq!(round_two_digits(p1.magnitude()), 17.69);
    }

    #[test]
    fn test_point_dist() {
        let p1 = Point::new(10, 10);
        let p2 = Point::new(14, 13);
        assert_eq!(round_two_digits(p1.dist(p2)), 5.00);
    }

    #[test]
    fn test_point_add() {
        let p1 = Point::new(16, 16);
        let p2 = p1 + Point::new(-4, 3);
        assert_eq!(p2, Point::new(12, 19));
    }

    #[test]
    fn test_polygon_left_most_point() {
        let p1 = Point::new(12, 13);
        let p2 = Point::new(16, 16);

        let mut poly = Polygon::new();
        poly.add_point(p1);
        poly.add_point(p2);
        assert_eq!(poly.left_most_point(), Some(p1));
    }

    #[test]
    fn test_polygon_iter() {
        let p1 = Point::new(12, 13);
        let p2 = Point::new(16, 16);

        let mut poly = Polygon::new();
        poly.add_point(p1);
        poly.add_point(p2);

        let points = poly.iter().cloned().collect::<Vec<_>>();
        assert_eq!(points, vec![Point::new(12, 13), Point::new(16, 16)]);
    }

    #[test]
    fn test_shape_perimeters() {
        let mut poly = Polygon::new();
        poly.add_point(Point::new(12, 13));
        poly.add_point(Point::new(17, 11));
        poly.add_point(Point::new(16, 16));
        let shapes = vec![
            Shape::from(poly),
            Shape::from(Circle::new(Point::new(10, 20), 5)),
        ];
        let perimeters = shapes
            .iter()
            .map(Shape::perimeter)
            .map(round_two_digits)
            .collect::<Vec<_>>();
        assert_eq!(perimeters, vec![15.48, 31.42]);
    }
}